/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fork.modbox;

import fork.*;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import tal.drivechain.chain.Chain;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.chain.NodeListener;
import tal.drivechain.module.properties.ColorParser;
import tal.drivechain.plugin.Channel;
import tal.drivechain.variable.SetProperty.Flag;
import tal.drivechain.variable.Variable;
import tal.drivechain.variable.types.DataType;

/**
 *
 * @author eisental
 */
public class NodeCube extends JPanel implements NodeListener {

    public final static int NODE_IDLE = 0;
    public final static int NODE_RECEIVED_MSG = 1;
    public final static int NODE_SENT_MSG = 2;
    private int nodeState = NODE_IDLE;
    private boolean selected = false;
    private Modbox owner;
    private ChainNode node;
    private Color color, xor;
    private Jack inputJack, outputJack;
    private InfoPanel infoPanel;
    private boolean hasLocation = false;

    public NodeCube(ChainNode node, Modbox owner, Color color, Font font) {
        this.node = node;
        this.owner = owner;
        node.addNodeListener(this);
        setColor(color);
        setFont(font);
        this.setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();

        inputJack = new Jack(true, xor, this);
        c.gridx = 0; c.fill = GridBagConstraints.BOTH;
        add(inputJack, c);

        infoPanel = new InfoPanel(color, getFont(), this, owner);
        c.gridx = 1; c.fill = GridBagConstraints.BOTH;
        add(infoPanel, c);
        if (node.canHaveOutputs()) {
            outputJack = new Jack(false, xor, this);
            c.gridx = 2; c.fill = GridBagConstraints.BOTH;
            add(outputJack, c);
        }

        revalidate();
    }

    public void setColor(Color color) {
        this.color = color;
        this.xor = new Color(color.getRed() ^ 255, color.getGreen() ^ 255, color.getBlue() ^ 255);
        if (inputJack!=null) inputJack.setColor(xor);
        if (outputJack!=null) outputJack.setColor(xor);
        if (infoPanel!=null) infoPanel.setColor(color);
        this.repaint();
    }

    public int getModState() { return nodeState; }

    @Override
    public void setFont(Font font) {
        super.setFont(font);
        if (infoPanel!=null)
            infoPanel.setFont(font);
        if (this.getGraphics() != null) {
            setSize(getPreferredSize());
        }
    }

    public void setSelected(boolean selected) {
        if (this.selected==selected) return;

        this.selected = selected;
        if (selected) owner.getGUI().getGraph().getSelectedCubes().add(this);
        else owner.getGUI().getGraph().getSelectedCubes().remove(this);
        repaint();
        owner.getGUI().getGraph().repaint();
    }

    public ChainNode getChainNode() { return node; }

    @Override
    public void setLocation(int x, int y) {
        super.setLocation(x, y);
    }


    @Override
    public void addNotify() {
        super.addNotify();

        infoPanel.setSize(infoPanel.getPreferredSize());
        if (outputJack!=null)
            outputJack.setSize(outputJack.getPreferredSize());
        if (inputJack!=null)
            inputJack.setSize(inputJack.getPreferredSize());
        setSize(getPreferredSize());

        NodeGraph g = owner.getGUI().getGraph();

        if (g.getWidth()==0 || g.getHeight()==0) return;
        
        if (!hasLocation) {
            int maxX = g.getParent().getWidth() - getWidth();
            int maxY = g.getParent().getHeight() - getHeight();
            Point random = new Point((int)(25+Math.round(Math.random()*(maxX-50))),
                    (int)(Math.round(25+Math.random()*(maxY-50))));
            setLocation(random);

            hasLocation = true;
            owner.updateChainMeta();
        }

        revalidate();
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension infoD = infoPanel.getPreferredSize();
        int w = infoD.width + 5;
        if (node.canHaveOutputs()) w+=5;
        return new Dimension(w, infoD.height);
    }

    public void messageReceived(ChainNode src, Variable var, Channel channel, long timestamp) {
        nodeState = NODE_RECEIVED_MSG;
        repaint();
        this.repaint();
        if (var.getName().equals("exit")) return;
        if (owner.getDebugFlag()) {
            new Thread() {
                @Override
                    public void run() {
                    try { sleep(300); } catch (InterruptedException ex) {}
                    nodeState = NODE_IDLE;
                    repaint();
                }
            }.start();
        }
    }

    public void messageSent(ChainNode src, Variable var, long timestamp) {
        nodeState = NODE_SENT_MSG;
        this.repaint();
        if (owner.getDebugFlag()) {
            new Thread() {
                @Override
                public void run() {
                    try { sleep(300); } catch (InterruptedException ex) {}
                    nodeState = NODE_IDLE;
                    repaint();
                }
            }.start();
        }
    }

    public void connectionsChanged(ChainNode src) {
        owner.getGUI().getGraph().repaint();
    }

    void createLink(MouseEvent e, boolean input) {
        Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), owner.getGUI().getGraph());
        owner.getGUI().getGraph().removeTempLine();
        Component hit = owner.getGUI().getGraph().getComponentAt(p.x, p.y);
        if (hit instanceof NodeCube) {
            try {
                ChainNode dest = ((NodeCube)hit).node;
                if (dest==node) return;
                if (!input) // connect src output to dest input
                    node.addOutput(dest);
                else // connect dest output to src input
                    dest.addOutput(node);
            } catch (IllegalArgumentException ie) {
                owner.log(ie.getMessage(), ChainNode.MSG.ERROR);
                ie.printStackTrace();
            }
        }
        owner.getGUI().getGraph().repaint();
    }

    InfoPanel getInfoPanel() {
        return infoPanel;
    }

    public boolean isSelected() { return selected; }

    public void nameChanged(ChainNode src) {
        this.setSize(getPreferredSize());
        owner.getGUI().getGraph().repaint();
        owner.updateChainMeta();
    }

    public void metaChanged(ChainNode src) {
    }

    public void nodeDestroyed(ChainNode src) {
        owner.getGUI().getGraph().repaint();
        owner.updateChainMeta();
    }

    public void nodeMoved(ChainNode src) {
        owner.getGUI().getGraph().repaint();
        owner.updateChainMeta();
    }

    public void propertyChanged(ChainNode src, String name, Object value) {}

    void setHasLocation(boolean b) {
        hasLocation = b;
    }
}

class Jack extends JComponent implements MouseListener, MouseMotionListener {
    boolean input;
    final static int STATE_IDLE = 0, STATE_HOVER = 1, STATE_DRAG = 2;
    int state = STATE_IDLE;
    Color idleColor, hoverColor, dragColor;
    NodeCube cube;

    public Jack(boolean input, Color color, NodeCube cube) {
        this.cube = cube;
        this.input = input;
        setColor(color);
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension d = cube.getInfoPanel().getPreferredSize();
        if (d==null)
            return new Dimension(5,10);
        else return new Dimension(5, d.height);
    }

    public void setColor(Color color) {
        this.idleColor = color;
        this.hoverColor = color.brighter().brighter();
        this.dragColor = new Color(color.getRed()^255, color.getGreen()^255, color.getBlue()^255);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (state==STATE_IDLE)
            g.setColor(idleColor);
        else g.setColor(hoverColor);
        g.fillRect(0,0,getWidth(),getHeight());

        if (state==STATE_DRAG) {
            g.setColor(dragColor);
            g.drawRect(0, 0, getWidth()-1, getHeight()-1);
        }

    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
        state = STATE_IDLE;
        repaint();
        cube.createLink(e, input);
    }

    public void mouseEntered(MouseEvent e) {
        state = STATE_HOVER;
        repaint();
    }

    public void mouseExited(MouseEvent e) {
        state = STATE_IDLE;
        repaint();
    }

    public void mouseDragged(MouseEvent e) {
        state = STATE_DRAG;
        NodeGraph graph = (NodeGraph)cube.getParent();
        Point p1, p2;
        if (!input)
            p1 = SwingUtilities.convertPoint(cube, new Point(cube.getWidth(), cube.getHeight()/2), graph);
        else
            p1 = SwingUtilities.convertPoint(cube, new Point(0, cube.getHeight()/2), graph);
        p2 = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), graph);
        graph.setTempLine(p1, p2);
        cube.getParent().getGraphics().drawLine(p1.x, p1.y, p2.x, p2.y);
        cube.getParent().repaint();

        repaint();
    }

    public void mouseMoved(MouseEvent e) {
    }

}
class InfoPanel extends JComponent implements MouseMotionListener, MouseListener {
    private final static int STATE_IDLE = 0;
    private final static int STATE_HOVER = 1;
    private int state = STATE_IDLE;
    private NodeCube cube;
    private Color color, msgIn, msgOut, xor;
    private Font typeFont, instanceFont;
    int xDiff = 0, yDiff = 0;
    private boolean change;
    private Modbox owner;

    public InfoPanel(Color color, Font font, NodeCube cube, Modbox owner) {
        this.owner = owner;
        this.cube = cube;
        setColor(color);
        setFont(font);
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
    }

    @Override
    public Dimension getPreferredSize() {
        Graphics g = getGraphics();
        if (g==null) return 
            new Dimension(0, 0);
        
        String typeString = cube.getChainNode().getNodeClassName();
        int widthInstance = g.getFontMetrics(instanceFont).stringWidth(cube.getChainNode().getName()) + 8;
        int widthModType = g.getFontMetrics(typeFont).stringWidth(typeString) + 8;
        int w = Math.max(widthInstance, widthModType);
        int h = g.getFontMetrics(instanceFont).getHeight() + 2 + g.getFontMetrics(typeFont).getHeight() + 2;
        return new Dimension(w, h);
    }

    public void setColor(Color color) {
        this.color = color;
        msgIn = new Color(color.getGreen(), color.getBlue(), color.getRed());
        msgOut = new Color(color.getBlue(), color.getRed(), color.getGreen());
        xor = ColorParser.xor(color);
        repaint();

    }

    @Override
    public void setFont(Font font) {
        super.setFont(font);
        instanceFont = font.deriveFont((float)(font.getSize2D()*1.1));
        typeFont = font.deriveFont(Font.BOLD);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        int typeHeight = g.getFontMetrics(typeFont).getHeight();
        int instanceHeight = g.getFontMetrics(instanceFont).getHeight();
        Color curColor;
        if (cube.getModState()==NodeCube.NODE_RECEIVED_MSG && owner.getDebugFlag())
            curColor = this.msgIn;
        else if (cube.getModState()==NodeCube.NODE_SENT_MSG && owner.getDebugFlag())
            curColor = this.msgOut;
        else curColor = color;

        if (state != STATE_IDLE)
            curColor = curColor.brighter().brighter();
        if (cube.isSelected())
            g.setColor(Color.BLACK);
        else g.setColor(curColor);
        g.fillRect(0, 0, getWidth(), getHeight());
        if (cube.isSelected()) {
            g.setColor(curColor);
            ((Graphics2D)g).setStroke(new BasicStroke(2));
            g.drawRect(0, 0, getWidth(), getHeight());
        } else g.setColor(Color.BLACK);
        g.setFont(typeFont);

        String typeString = cube.getChainNode().getNodeClassName();
        g.drawString(typeString, 4, typeHeight-1);
        g.setFont(instanceFont);
        g.drawString(cube.getChainNode().getName(), 4, typeHeight+instanceHeight-1);
    }
    
    public void mouseDragged(MouseEvent e) {
        Point oldLoc = cube.getLocation();
        Point p = SwingUtilities.convertPoint(this,
                e.getPoint(), cube.getParent());
        p.x = p.x - xDiff;
        p.y = p.y - yDiff;
        if (p.x < 0) {
            p.x = 0;
        }
        if (p.y < 0) {
            p.y = 0;
        }
        cube.setLocation(p);

        if (cube.isSelected()) {
            int xlocDiff = p.x - oldLoc.x;
            int ylocDiff = p.y - oldLoc.y;
            for (NodeCube c : owner.getGUI().getGraph().getCubes()) {
                if (c.isSelected() && c!=cube) {
                    Point curLocation = c.getLocation();
                    Point newLocation = new Point(curLocation.x+xlocDiff, curLocation.y+ylocDiff);
                    c.setLocation(newLocation);
                }
            }
        }
        change = true;

    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getButton()==MouseEvent.BUTTON1) {
            // shift - just select, no modifier - select and deselect everything, ctrl - de select if selected
            if (e.isShiftDown())
                cube.setSelected(!cube.isSelected());
            else {
                if (e.getClickCount()==1) {
                    owner.getGUI().getGraph().clearSelection();
                    cube.setSelected(true);
                } else if (e.getClickCount()==2) {
                    if (cube.getChainNode() instanceof Chain) {
                        Chain c = (Chain)cube.getChainNode();
                        owner.setActualBoxChain(c);
                    } else if (cube.getChainNode().getNodeClassName().equals("gui.Window"))
                        cube.getChainNode().send(Variable.parseVariable("fromModbox", null, "show{}", false), -1, Channel.CHAIN);
                }
            }
            owner.sendSelectionChange();
        } else if (e.getButton()==MouseEvent.BUTTON3) {
            // open the node popup frame
            if (!cube.isSelected()) {
                owner.getGUI().getGraph().clearSelection();
                cube.setSelected(true);
                owner.sendSelectionChange();
            }
            owner.getGUI().showNodePopup(cube.getChainNode(), e.getLocationOnScreen());
        }
    }

    public void mousePressed(MouseEvent e) {
        xDiff = e.getX();
        yDiff = e.getY();
    }

    public void mouseReleased(MouseEvent e) {
        if (change) {
            owner.getGUI().getGraph().updateSize();
            owner.updateChainMeta();
            change = false;
        }
    }

    public void mouseEntered(MouseEvent e) {
        state = STATE_HOVER;
        repaint();
    }

    public void mouseExited(MouseEvent e) {
        state = STATE_IDLE;
        repaint();
    }

}
